Skip to content

[DES-196] Adapter webhook dispatch (Part B)#3

Merged
tarasyarema merged 13 commits into
mainfrom
des-196-adapter-webhook-dispatch
Apr 22, 2026
Merged

[DES-196] Adapter webhook dispatch (Part B)#3
tarasyarema merged 13 commits into
mainfrom
des-196-adapter-webhook-dispatch

Conversation

@tarasyarema

Copy link
Copy Markdown
Contributor

Summary

Closes the last gap blocking the v0.1.0 publish: adapter event dispatch.

  • Slack + GChat get a full Part-B port (webhook + Socket Mode for Slack; HTTP + Pub/Sub for GChat; both adapters gain handle_webhook / post_message / edit_message / delete_message / add_reaction / remove_reaction / streaming).
  • Discord, GitHub, Teams, Linear, Telegram, WhatsApp keep their existing dispatch and pick up protocol-conformance tests + E2E skeletons. Each intentional NotImplementedError stub (Teams 9, WhatsApp 7, Linear 7, Telegram 3, + Discord/GChat/GitHub open_modal/channel-surface stubs) is pinned by a test and documented in docs/parity.md.
  • chat.types.Adapter is a real @runtime_checkable Protocol (was Any). isinstance(create_*_adapter(), Adapter) passes for all 8 adapters.
  • Chat.handle_webhook is covered by a cross-adapter parametrized matrix in chat-integration-tests.
  • docs/parity.md has a "Dispatch surface" section; a self-test enforces table/package drift can't land silently.

Scope notes

  • Plan & autopilot execution: thoughts/taras/plans/2026-04-22-des-196-adapter-webhook-dispatch.md (status flipped to completed).
  • TDD: 11 phases × 3–11 cycles, RED → GREEN → COMMIT per phase.
  • Live Slack E2E confirmed end-to-end against Taras's real workspace (url_verification handshake → @Localillo mention → bot replies → in-thread subscribe/echo). Other provider E2Es ship as runnable skeletons gated on creds.

Commit trail

Phase SHA Area
0 492652d Adapter Protocol + parity.md + CHANGELOG
1 83328a1 Slack webhook (Events API + interactivity + outbound + streaming)
2 613c546 Slack Socket Mode
3 d3f892a GChat (HTTP + Pub/Sub + Card v2 + fetch)
4 d0e3ba8 Discord audit (Author-dict bug fix) + E2E skeleton
5 7b532b7 GitHub audit + E2E skeleton
6 db632a5 WhatsApp audit + stub pinning + E2E skeleton
7 76c3244 Teams audit + stub pinning + E2E skeleton
8 f734287 Linear audit + E2E skeleton
9 c93342f Telegram audit + stub pinning + E2E skeleton
10 0bf22e1 Cross-adapter Chat.handle_webhook matrix + parity self-test
56829d6 Live-E2E followup: _common.py FastAPI Request resolution, auth.test() to auto-populate bot_user_id, echo handler arity

Totals

  • 55 files changed, ~7700 lines added
  • 2200 tests passing across 15 packages
  • ruff check, ruff format --check, pytest packages/ all green
  • mypy packages/chat/src: 32 pre-existing errors, baseline unchanged (documented as known gaps)

Test plan

  • Full validation suite: uv run ruff check packages/ && uv run ruff format --check packages/ && uv run pytest packages/
  • Cross-adapter parametrized matrix (8 adapters × canned webhook → expected handler)
  • Parity doc self-test (every parity.md row maps to a real package, and vice versa)
  • Live Slack E2E: uv sync --group e2e && uv run python examples/e2e/slack/echo.py + ngrok → real workspace mention + thread echo
  • Optional live E2Es for Discord / GitHub / WhatsApp / Teams / Linear / Telegram / GChat via their respective examples/e2e/<adapter>/echo.py scripts (each sys.exit()s with a clear message on missing creds — not gating for v0.1.0)
  • Slack Socket Mode live check (SLACK_APP_TOKEN=xapp-... uv run python examples/e2e/slack/echo.py; --mode socket flag on echo.py is a nice-to-have followup; the adapter supports it via config already)

Ticket: https://linear.app/desplega-labs/issue/DES-196

Port GoogleChatAdapter Part B to match upstream adapter-gchat/src/index.ts:

- handle_webhook accepts both direct HTTP MESSAGE/ADDED_TO_SPACE/REMOVED_FROM_SPACE
  events and Pub/Sub push envelopes, funneling both into _dispatch_event.
- New pubsub.py helper detects envelope shape and unwraps CloudEvents metadata
  so the Workspace Events Pub/Sub flow routes through the same handler chain.
- Outbound surface: post_message (plain text + Card v2), edit_message with
  update_mask=text,cards_v2, delete_message, add_reaction / remove_reaction
  (emoji → Unicode via DEFAULT_EMOJI_MAP), fetch_messages with pageToken/pageSize
  pagination, fetch_channel_info, fetch_channel_messages, list_threads, open_dm,
  start_typing, stream (placeholder → periodic edit → final flush).
- open_modal raises chat.NotImplementedError with feature="modals" (Google Chat
  has no Slack-style modals; upstream uses Card v2 inline instead).
- REST dispatch via pluggable _rest_client (tests inject SimpleNamespace +
  AsyncMock; production falls back to an httpx-backed attribute-walk shim).
- ADDED_TO_SPACE triggers create_space_subscription when pubsub_topic is set;
  REMOVED_FROM_SPACE tears the cached subscription down.
- Adapter Protocol conformance: adds initialize, disconnect, post_channel_message,
  subscribe/unsubscribe, get_channel_visibility; declares lock_scope="thread"
  and persist_message_history=False class attrs.

Tests: new tests/test_dispatch.py with 16 cycles covering 3.2–3.8 plus the
Phase 0 conformance test now flips GREEN (152 gchat tests pass).

E2E skeleton at examples/e2e/gchat/echo.py — mirrors slack/echo.py shape,
ast.parses cleanly, exits with clear message when required env vars missing.
- LinearAdapter now conforms to chat.types.Adapter Protocol
  (added lock_scope, persist_message_history, disconnect, subscribe,
  unsubscribe, post_channel_message, fetch_channel_info,
  fetch_channel_messages, list_threads, open_dm, open_modal, is_dm,
  get_channel_visibility).
- Unsupported surfaces raise chat.errors.NotImplementedError with
  feature= kwarg (matches Teams/WhatsApp pattern).
- remove_reaction upgraded from a silent warn no-op to a pinned
  chat.NotImplementedError(feature='removeReaction') stub; add_reaction
  remains fully implemented (Linear reactionCreate).
- test_dispatch.py pins Comment webhook round-trip via Chat.handle_webhook
  (HMAC-SHA256 verified end-to-end, no monkeypatch) and the Author
  dataclass shape (guards against author-dict regression).
- examples/e2e/linear/echo.py skeleton — LINEAR_API_KEY +
  LINEAR_WEBHOOK_SECRET env vars, fails clearly when missing.
- docs/parity.md: Linear 'react' column updated to 'partial' with a
  dedicated stub-listing subsection for the 7 Linear stubs.
…f-test

Add a parametrised cross-adapter dispatch test that routes canned webhook
bodies through ``Chat.handle_webhook(<name>, body, headers)`` for all 8
shipped adapters (slack / gchat / discord / github / whatsapp / teams /
linear / telegram) and asserts the expected handler fires. Body fixtures
live under ``tests/__fixtures__/`` with provenance comments; signature-
bearing adapters (Slack / GitHub / WhatsApp / Linear) compute HMAC
signatures dynamically against known secrets, while Ed25519 (Discord)
and JWT (Teams / GChat) verifiers are monkeypatched at factory time.

Also add a parity-doc self-test that walks ``packages/`` and asserts
every ``## Dispatch surface`` row in ``docs/parity.md`` corresponds to a
real ``chat-adapter-<name>`` module — catches stale parity entries.

Closes DES-196.
Three bugs surfaced running examples/e2e/slack/echo.py against a real
workspace:

- examples/e2e/_common.py: FastAPI could not resolve the `Request` type
  annotation on the webhook handler — imports lived inside the function
  scope but `from __future__ import annotations` stringifies hints, so
  FastAPI fell back to treating `request` as a query parameter and
  returned 422 before the body was ever parsed. Move the fastapi /
  uvicorn / Response imports to module level behind a defensive ImportError
  guard.

- packages/chat-adapter-slack/src/chat_adapter_slack/adapter.py:
  SlackAdapter.initialize() now calls auth.test() to populate
  bot_user_id / bot_id when they weren't passed via config. Mirrors
  upstream's initialize — without this, Chat._detect_mention can never
  match `<@U_BOT>` on plain `message.channels` events and the
  `on_new_mention` handler never fires for non-app_mention traffic.

- examples/e2e/slack/echo.py: handlers take (thread, message, context),
  matching Chat._dispatch_to_handlers; use `author.user_id` instead of
  the nonexistent `author.id`.

Plus ruff RUF100 / format cleanup across the sibling echo skeletons.

214 Slack tests still green. Manual E2E verified live — Slack
url_verification 200 + @-mention dispatch + in-thread subscribe/echo.
Adds the `--mode webhook|socket` flag the original plan flagged as a
nice-to-have. `examples/e2e/_common.py` gains a `run_socket_client`
helper that calls `Chat.initialize` (which in turn drives
`SlackAdapter.connect`, opening the websocket), then blocks on
SIGINT/SIGTERM and cleans up via `adapter.disconnect`.

`--mode socket` requires SLACK_APP_TOKEN; defaults to webhook. Picks up
$SLACK_MODE as a default if no flag is passed.

Live-verified end-to-end: websocket connects, `auth.test` resolves
identity, `apps.connections.open` succeeds, Slack routes @-mention +
thread-message events through the socket, handlers fire, bot replies
in-thread — no ngrok required.
@tarasyarema tarasyarema merged commit 0c54ff1 into main Apr 22, 2026
1 check passed
@tarasyarema tarasyarema deleted the des-196-adapter-webhook-dispatch branch April 22, 2026 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant